React Development Basic -- 组件开发基础

React Development Basic -- 组件开发基础

目标:

  1. 组件概述
  2. 复用组件
  3. 组件模板
  4. 组件状态
  5. 组件通讯
  6. 表单控制
  7. 样式控制
  8. 传送门组件

1. 组件概述

目标:

  1. 什么是组件
  2. 组件的设计思想
  3. 如何创建组件

1.1 什么是组件

​ 在一个页面中,如果将全部逻辑代码、页面的布局代码、样式代码等放在一起,这样处理起来会很麻烦,并且不利于后续管理和扩展。

​ 于是乎,产生了一种开发思路:组件化开发

​ 组件化开发就是将一个页面拆分为一个个功能模块,每个功能完成自己这部分的独立功能。

整体过程为:

  • 将一个完整的页面分为很多组件
  • 每个组件用于实现页面中的一个功能模块
1. React 的组件化

React 中的组件就是页面中的一小块区域,组件内部会包含这块区域中的视图代码、样式代码以及逻辑代码。

image-20220325161026819

React 采用组件化的方式构建用户界面

image-20220325161042749

image-20220325161116982

​ 组件的另一个核心思想就是解耦,默认情况下每个组件都有自己的作用域,内部代码在外部是不可见的,这意味着组件之间的代码不会发生冲突,从而避免在传统开发中经常出现的改A坏B的问题。

05

1.3 如何创建组件

在 React 中组件以函数的形式存在,组件最基本的特征就是一个返回视图(JSX)的函数。

示例:

// 组件的首字母必须为大写
function App() {
    return <div> Hello World!!! </div>
}
1
2
3
4

在 React 中,组件以自定义标签的形式进行调用:

ReactDOM.render(<App />, document.getElementById("root"))
1

多次调用 App 组件:

const jsx = (
  <>
    <App />
    <App />
  </>
)
ReactDOM.render(jsx, document.getElementById("root"))
1
2
3
4
5
6
7

组件和 HTML 一样,都是以树状结构存在的。

06

示例:

![create components](https://raw.githubusercontent.com/Sue-52/PicGo/main/images/create components.png)

2. 复用组件

目标:

  1. props
  2. children

2.1 Props 继承

在调用组件时,通过为组件标签添加属性的方式向子组件内部传递数据,实现组件的差异化和复用。

示例:

<ChildProps msg="first" />
<ChildProps msg="second" />
1
2

在子组件内部,通过组件函数参数接收父组件中传递的数据,组件函数中的第一个参数是对象类型,可以进行属性解构,存储了父组件中传来的所有数据。

示例1:

function ChildProps (props) {
    return <p> {props.msg} </p>
}
1
2
3

示例2:

function ChildProps ({ msg }) {
    return <p> { msg } </p>
}
1
2
3

子组件中的 props 参数可以设置为默认值,这样调用组件时就可以根据子组件内部所需内容进行传递,当数据传递了,则使用父组件传递的数据,没有则使用默认数据

示例:

ChildProps.defaultProps = {
    msg: "Hello React"
}
1
2
3

使用时:

<ChildProps msg="first" /> // first
<ChildProps /> // Hello React
1
2

子组件中的 props 默认值可以防止内部代码执行出错。

错误示例:

function Person (props) {
  return <div>{props.info.name}</div>
}
1
2
3
<Person /> // 报错, 因为组件内部代码执行时, 通过 props 获取到的 info 为 undefined, 不能再通过 undefined 获取 name
<Person info={{name: "张三"}}/> // 张三
1
2

正确示例:

Person.defaultProps = {
  info: {}
}
1
2
3
<Person /> // 不会报错, 因为在 JSX 中渲染 undefined 是合法的
<Person info={{name: "张三"}}/> // 张三
1
2

2.2 children 组合

在调用组件时,可以向组件内部添加 JSX,实现差异化组件复用,相比于 props,使用 children 可以向子组件内部传递更复杂的数据。

示例1:

<Person> Hello, React Children Property </Person>
1

示例2:

<Person> 
	<p> single child </p>
</Person>
1
2
3

示例3:

<Person> 
	<p> single child </p>
    <p> single child </p>
    <p> single child </p>
</Person>
1
2
3
4
5

Person 组件:

function Person(props) {
    return <div> { props.children } </div>
}
1
2
3

能够传递任何的JavaScript表达式作为children,包括函数。其中 children 的最常用的场景就是辅助创建布局组件。布局组件可以增强组件复用能力。

示例:

function HomePage() {
  return <div>HomePage</div>;
}
function AboutPage() {
  return <div>AboutPage</div>;
}
1
2
3
4
5
6
function Header() {
  return <div>Header</div>;
}
function Footer() {
  return <div>Footer</div>;
}
1
2
3
4
5
6
function Layout(props) {
  return (
    <>
    <Header />
    <div>{props.children}</div>
    <Footer />
    </>
  );
}
1
2
3
4
5
6
7
8
9
function HomePage() {
  return <Layout>HomePage</Layout>;
}
function AboutPage() {
  return <Layout>AboutPage</Layout>;
}
1
2
3
4
5
6

3. 组件模板

目标:

  1. 事件程序
  2. 条件渲染
  3. 列表渲染

3.1 事件程序

目标:

  1. 添加事件
  2. 事件传参
  3. 事件对象
1. 添加事件

在 JSX 中,为元素添加事件:

  • React 事件的命名采用小驼峰式(camelCase),而不是纯小写。
  • 使用 JSX 语法时你需要传入一个函数作为事件处理函数,而不是一个字符串。
  • 事件处理函数通过插值的方式指定。

示例:

function App() {
    const handleClickButton = () => {
        console.log("Hello React!")
    }
    return <button onClick={handleClickButton}> button </button>
}
1
2
3
4
5
6

在 React 中另一个不同点是你不能通过返回 false 的方式阻止默认行为。你必须显式的使用 preventDefault

2. 事件传参

在 JSX 中,为事件添加参数和 javascript 中调用函数思路一致。

function App() {
    const handleClick = (str1, str2) => {
        console.log(str1, str2);
    };
    return <button onClick={() => handleClickButton("Hello", "React")}> button </button>
}
1
2
3
4
5
6
3. 事件对象 event
  • 事件处理函数,在没有传递参数时,第一个参数默认为事件对象

    function App () {
     	const onClickHandler = (event) => {
            console.log(event)
        }
      	return <button onClick={onClickHandler}> button </button>
    }
    
    1
    2
    3
    4
    5
    6
  • 事件处理函数如果传入了参数的情况,默认最后一个参数为事件对象

    function App () {
      	const onClickHandler = (arg1, arg2, event) => {
         	 console.log(arg1, arg2, event)
      	}
     	 return <button onClick={() => onClickHandler('a', 'b')}> button </button>
    }
    
    1
    2
    3
    4
    5
    6
  • 在调用事件处理函数时,可以将事件对象以参数显性传递

    function App () {
      	const onClickHandler = (arg1, event, arg2) => {
            console.log(arg1, event, arg2)
        }
     	 return <button onClick={(event) => onClickHandler('a', event, 'b')}> button </button>
    }
    
    1
    2
    3
    4
    5
    6

2.3.2 条件渲染

目标:

  1. If 条件判断
  2. 三元运算符

在组件模板中, 根据条件的不同渲染不同的视图元素。

1. If 条件判断

The if statement executes a statement if a specified condition is true. If the condition is false, another statement can be executed.

当指定条件为真,if 语句会执行一段语句。如果条件为假,则执行另一段语句

示例1:

function OnMessage() {
  	return <span>The Machine is On</span>
}
function OffMessage() {
  	return <span>The Machine is Off</span>
}
function OnOff() {
  	if (true) {
    	return <OnMessage />
  	} else {
    	return <OffMessage />
  	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13

示例2:

function Message() {
    const getMessage() {
        if(true) {
            return <OnMessage />
        }else {
            return <OffMessage />
        }
    }
    return {getMessage()}
}
1
2
3
4
5
6
7
8
9
10

示例3:

function Machine() {
  	let message = null
  	if (true) {
    	message = <OnMessage />
  	} else {
    	message = <OffMessage />
  	}
  	return <p>{message}</p>
}
1
2
3
4
5
6
7
8
9
2. 三元运算符

The conditional (ternary) operator is the only JavaScript operator that takes three operands: a condition followed by a question mark (?), then an expression to execute if the condition is true followed by a colon (😃, and finally the expression to execute if the condition is false. This operator is frequently used as an alternative to an if...else statement.

条件(三元)运算符是 JavaScript 仅有的使用三个操作数的运算符。一个条件后面会跟一个问号(?),如果条件为 true ,则问号后面的表达式A将会执行;表达式A后面跟着一个冒号(:),如果条件为 false ,则冒号后面的表达式B将会执行。本运算符经常作为 if 语句的简捷形式来使用。

示例1:

function Message() {
    return <p> {true ? <OnMessage /> : <OffMessage />} </p>
}
1
2
3

示例2:

const isError = true
function Message() {
    <div className={isError ? "error" : "success"}>
    	{isError ? <p>Something went wrong...</p> : <p>Everythis is ok</p>}
    </div>
}
1
2
3
4
5
6

2.3.3 列表渲染

目标:

  1. 数组自动展开
  2. 列表渲染(map方法)
  3. key 属性
1. 数组自动展开

在 JSX 中,可以直接将数组放入插值表达式中,因为数组会被自动展开,数组中的元素追直接渲染到该位置。

示例1:

function Home() {
  const data = ["The beach", "The mountains", "Vibrant cities", "Roughing it"];
  return (
    <>
      <div>{data}</div>
    </>
  );
}
1
2
3
4
5
6
7
8

image-20220325161251268

示例2:

function Home() {
  const jsxList = [
    <li>list-item-1</li>,
    <li>list-item-2</li>,
    <li>list-item-3</li>,
  ];
  return (
    <>
      <ul>{jsxList}</ul>
    </>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12

image-20220325161315285

注意:不能直接将对象数组放置在插值语法中,因为当数组被展开后,JSX 不知道要如何渲染对象。

示例:

const data = [
  { name: "The beach" },
  { name: "The mountains" },
  { name: "Vibrant cities" },
  { name: "Roughing it" }
]
1
2
3
4
5
6

image-20220325161329931

2. 列表渲染(map方法)

map() 方法返回一个新数组,数组中的元素为原始数组元素调用函数处理后的值。

map() 方法按照原始数组元素顺序依次处理元素。

注意: map() 不会对空数组进行检测。

注意: map() 不会改变原始数组。

参数说明:

  • 第一个参数:函数(必须)
    • currentValue,当前元素的值(必须)
    • index:当前元素的索引值(可选)
    • arr:当前元素属于的数组对象(可选)
  • thisValue:可选。对象作为该执行回调时使用,传递给函数,用作 "this" 的值。 如果省略了 thisValue,或者传入 null、undefined,那么回调函数的 this 为全局对象。

返回值:一个新的数组

示例:

const arr = [1, 2, 3, 4, 5, 6];
const newArr = arr.map((item, index) => item * index);
console.log(newArr); // [0, 2, 6, 12, 20, 30]
1
2
3

map方法内部会对原数组进行遍历,在遍历的过程中不断调用传递到 map 方法中的回调函数,并将当前遍历的值和索引值传递给回调函数,回调函数的返回值会被存储到一个新的数组中,结束遍历后返回到新的数组中。

Array.prototype.myMap = function (callback) {
  var result = []
  for (var i = 0; i < this.length; i++) {
    result.push(callback(this[i], i))
  }
  return result
}
1
2
3
4
5
6
7

map 方法在 JSX 中的应用:将数组中的内容渲染到 ul 标签中, 内容使用 li 标签包裹。

示例1:

function Home() {
  let data = ["Data1", "Data2", "Data3"];

  return (
    <>
      <ul>
        {data.map((item, index) => (
          <li key={index}>{item}</li>
        ))}
      </ul>
    </>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13

image-20220325161348665

渲染过程图:

08

3. key 属性

在列表渲染的过程中,React 建议开发者为列表项添加 key 属性,否则在控制台中输出警告。

image-20220325161428037

key 属性用于为列表中的元素提供标识,React通过标识来识别元素发生的变化,实现元素的更增删改查,以此提升性能。

  • 当列表渲染时没有key属性,则当数据更新时元素会发生整体的重新渲染。

  • 当列表有 key 属性时,当数据更新时,只有变化的元素会重新渲染,其余元素则会被复用。

  • 并且 key 值必须不为重复的,一般以id作为key值

如果列表只在组件初始化的时候渲染一次,后续不发生变化,也可以使用循环索引作为 key 属性的值。

17

4. 组件状态

目标:

  1. 组件状态概述
  2. 钩子函数概述
  3. useState
  4. useEffect

4.1 组件状态概述

在现实世界中,状态通常是指人或事物表现出来的形态。

由于组件执行的最终结果就是用户界面,所以组件状态指的就是用户界面的状态。

21

React 遵循数据驱动DOM的理念,数据的变化会引起试图的更新。在React中,有两个核心的默认属性,分别是state和props。state会记录组件的状态,React根据状态的变化,会对界面做相应的调整或渲染。props则是数据流向属性,React通过props传递来实现父子组件之间的通信。

所谓的数据则是该组件的状态数据,组件状态数据其实就是组件内部维护的能够驱动视图更新的数据。

4.2 钩子函数概述

目标:

  1. 命名规范
  2. 思想
  3. 代码组织

“hooks” 直译为:“钩子”,是系统在运行到某一时期时,会调用被注册到该师级的回调函数。

比较常见的钩子有:windows 系统的钩子能监听到系统的各种事件,浏览器提供的 onloadaddEventListener 能注册在浏览器各种时机被调用的方法。

在react中的 hooks 为:

一系列以 “use” 作为开头的方法,它们提供了让你可以完全避开 class式写法,在函数式组件中完成生命周期、状态管理、逻辑复用等几乎全部组件开发工作的能力。

就是说提供了在函数式组件中完成开发工作的能力。

React 使用函数作为组件,但是函数自身有重大限制,就是不能持续保存状态,因为函数在执行完成以后内部的局部变量就被释放了。

image-20220325161600344

其钩子具有保存状态的功能,因为只有状态被保存了,我们才可以基于现有状态计算新状态,使用新的状态更新视图。

1. 命名规范

通常来说,hooks 的命名都是以 use 作为开头,这个规范也包括了那么我们自定义的 hooks

2. 思想

react 官方文档里,对 hooks 的定义和使用提出了 “一个假设、两个只在” 核心指导思想。

  • 一个假设: 假设任何以 「use」 开头并紧跟着一个大写字母的函数就是一个 Hook
  • 第一个只在: 只在 React 函数组件中调用 Hook,而不在普通函数中调用 Hook。(Eslint 通过判断一个方法是不是大坨峰命名来判断它是否是 React 函数)
  • 第二个只在: 只在最顶层使用 Hook,而不要在循环,条件或嵌套函数中调用 Hook。

因为是约定,因此 useXxx 的命名并非强制,你依然可以将你自定义的 hook 命名为 byXxx 或其他方式,但不推荐。

3. 代码组织

项目、模块、页面、功能,如何高效而清晰地组织代码。一个页面中,N件事情的代码在一个组件内互相纠缠确实是在 Hooks 出现之前非常常见的一种状态。

image-20220325161618693

(假设每一种颜色就是代码一种高度相关的业务逻辑)

无论是 vue 还是 react, 通过 Hooks 写法都能做到,将“分散在各种声明周期里的代码块”,通过 Hooks 的方式将相关的内容聚合到一起。

这样带来的好处是显而易见的:“高度聚合,可阅读性提升”。伴随而来的便是 “效率提升,bug变少”

4.3 useState

React.useState 方法用于声明组件状态数据,通过该方法声明状态数据被更新后触发视图的更新。

使用示例:

const [state, setState] = useState(initialState)
1
  • useState 有一个参数(initialState 可以是一个函数,返回一个值,但一般都不会这么用),该参数可以为任意数据类型,一般用作默认值.
  • useState 返回值为一个数组,数组的第一个参数为我们需要使用的 state,第二个参数为一个改变state的函数(功能和this.setState一样)

示例:

import React,{useState} from "react";
function Example() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}
export default Example;
1
2
3
4
5
6
7
8
9
10
11
  • 「第一行」: 引入 React 中的 useState Hook。它让我们在函数组件中存储内部 state。
  • 「第三行」:Example 组件内部,通过调用 useState Hook 声明了一个新的 state 变量。它返回一对值给到我们命名的变量上。把变量命名为 count,因为它存储的是点击次数。我们通过传 0 作为 useState 唯一的参数来将其初始化为 0。第二个返回的值本身就是一个函数。它让我们可以更新 count 的值,所以我们命名为 setCount
  • 「第七行」: 当用户点击按钮后,传递一个新的值给 setCount。React 会重新渲染 Example 组件,并把最新的 count 传给它。

组件状态数据发生变化后会触发视图更新,视图更新意味着组件函数被重新执行,虽然组件函数重新执行了,但是状态变量并没有被释放,组件状态在每次组件函数重新执行后被保留了下来。

组件中的普通变量就不具备此功能,普通变量被更改后不会触发视图更新,而且当真正的组件状态变化后组件函数重新执行,普通变量也被重置为初始值。

问题代码:

function App() {
  const [count, setCount] = React.useState(0);
  let number = 0;
  console.log("rerender", number);
  return (
    <>
      <p>{count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <button
        onClick={() => {
          number = number + 1;
          console.log("onClick", number);
        }}
        >
        {number}
      </button>
    </>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

解答:当页面每次触发视图的更新,普通变量都会重新执行并重置为初始值。

重点:useState的初始值只有第一次有效

const Hook =()=>{
    console.log('Hook render...')
    const [count, setCount] = useState(0)
    const [name, setName] = useState('rose')

    return(
        <div>
            <div>
                {count}
            </div>
            <button onClick={()=>setCount(count+1)}>update count </button>
            <button onClick={()=>setName('jack')}>update name </button>
            <Child data={name}/>
        </div>
    )
}

const Child = memo(({data}) =>{
    console.log('child render...', data)
    const [name, setName] = useState(data)
    return (
        <div>
            <div>child</div>
            <div>{name} --- {data}</div>
        </div>
    );
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

33

4.4 useEffect

目标:

  1. 副作用概述
  2. 使用概要
  3. useEffect 做了什么

Effect Hook 可以让你在函数组件中执行副作用(数据获取,设置订阅以及手动更改 React 组件中的 DOM 都属于副作用)操作。

使用示例:

useEffect(fn, array)
1

useEffect在初次完成渲染之后都会执行一次, 配合第二个参数可以模拟类的一些生命周期。

相当于类组件中的componentDidMountcomponentDidUpdatecomponentWillUnmount 这三个生命周期的集合体。

1. 副作用概述

组件的职责就围绕 props 和 state 计算用户界面所需要的状态数据,其他的和渲染用户界面逻辑没有关系的操作都属于副作用。

比如 Ajax Request、手动修改 DOM、localStorage、console.log、setInterval 等。

副作用本身不是错误代码,但是如果副作用代码在组件中放置的不是最佳位置,可能导致组件的性能变差。

2. 使用概要
  • useEffect 实现 componentDidMount

    import React, { useState, useEffect } from "react";
    function Example() {
      const [count, setCount] = useState(0);
      useEffect(() => {
        console.log("在组件初次挂载完成后执行");
      }, []);
      return (
        <div>
          <p>You clicked {count} times</p>
          <button onClick={() => setCount(count + 1)}>Click me</button>
        </div>
      );
    }
    export default Example;
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

    如果第二个参数为空数组,useEffect相当于类组件里面componentDidMount。

  • useEffect 实现 componentDidUpdate

    import React, { useState, useEffect } from "react";
    function Example() {
      const [count, setCount] = useState(0);
      useEffect(() => {
        console.log("在组件初次挂载完成后执行");
      });
      return (
        <div>
          <p>You clicked {count} times</p>
          <button onClick={() => setCount(count + 1)}>Click me</button>
        </div>
      );
    }
    export default Example;
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14

    如果不传第二个参数,useEffect 会在初次渲染和每次更新时,都会执行。

  • useEffect 实现 componentWillUnmount

    function App() {
      React.useEffect(() => {
        const timer = setInterval(() => {
          console.log("hello");
        }, 1000);
        // 该函数在组件卸载前被执行
        return () => clearInterval(timer);
      }, []);
      return (
        <>
          <button onClick={() => setCount(count + 1)}>{count}</button>
          <button onClick={() =>  ReactDOM.unmountComponentAtNode(document.getElementById("root"))}>卸载组件</button>
        </>
      );
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15

    effect 返回一个函数,React 将会在执行清除操作时调用它。

  • 控制 useEffect 执行

    import React, { useState, useEffect } from "react";
    function Example() {
      const [count, setCount] = useState(0);
      const [number, setNumber] = useState(1);
      useEffect(() => {
        console.log("只会在cout变化时执行");
      }, [count]);
      return (
        <div>
          <p>You clicked {count} times</p>
          <button onClick={() => setCount(count + 1)}>Click cout</button>
          <button onClick={() => setNumber(number + 1)}>Click number</button>
        </div>
      );
    }
    export default Example;
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    当第二个参数中添加了依赖,那么只有依赖发生了变化后,useEffect才会执行。

  • 使用多个 useEffect 实现关注点分离

    import React, { useState, useEffect } from "react";
    function Example() {
      useEffect(() => {
        // 逻辑一
      });
      useEffect(() => {
        // 逻辑二
      });
       useEffect(() => {
        // 逻辑三
      });
      return (
        <div>
          useEffect的使用
        </div>
      );
    }
    export default Example;
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    Hook 允许我们按照代码的用途分离他们, 而不是像生命周期函数那样。React 将按照 effect 声明的顺序依次调用组件中的每一个 effect。

  • useEffect 中使用异步函数

    /* 错误用法 ,effect不支持直接 async await*/
     useEffect(async ()=>{
            /* 请求数据 */
          const res = await getData()
     },[])
    
    1
    2
    3
    4
    5

    useEffect是不能直接用 async await 语法糖的。useEffect的回调参数返回的是一个清除副作用的clean-up函数。因此无法返回Promise,更无法使用 async/await。

    • 解决方法一(推荐):

      const App = () => {
        useEffect(() => {
          (async function getDatas() {
            await getData();
          })();
        }, []);
        return <div></div>;
      };
      
      1
      2
      3
      4
      5
      6
      7
      8
    • 解决方法二:

        useEffect(() => {
          const getDatas = async () => {
            const data = await getData();
            setData(data);
          };
          getDatas();
        }, []);
      
      1
      2
      3
      4
      5
      6
      7
3. useEffect 做了什么

useEffect 告诉了我们,当 React 组件需要在 渲染后 执行某些操作。React 会保存你传递的函数,并且在执行DOM更新之后调用。

5. 组件通讯

目标:

  1. 父子通讯
  2. 兄弟通讯
  3. JSX 扩展运算符
  4. Context

组件通讯指的是,当组件A中的数据需要流向组件B中并使用,则需要将组件A中的数据传递给组件B,然后组件B再进行使用。

其传递的方式有很多种,通过 propscontext回调函数 等等。

5.1 父子通讯

在 React 应用中,组件与组件之间传递状态数据采用的是单向数据流架构,这是构建 React 应用必须遵循的状态数据传递规范。

单向数据流指的是状态数据只能在一个方向上传递,就是从上到下,即从父组件到子组件。状态数据不能反向传递,也不能横传递。

image-20220325161701341

示例:

父组件:

function Father() {
    const [message, setMessage] = useState("Hello");
    // 调用子组件并传递数据
    return <Child msg={message} />
}
1
2
3
4
5

子组件:

function Child(props) {
    // 接收来自父组件的 msg 状态数据
    return <div> {props.msg} </div>
}
1
2
3
4

function Child({ msg }) {
    // 接收来自父组件的 msg 状态数据
    return <div> {msg} </div>
}
1
2
3
4

单项数据流意味着子组件无法直接更新父组件传递的状态来实现视图的更新,事实上,props传来的数据是只读的(read only)不能被修改。

若是子组件想要修改父组件传递下来的状态数据,必须要将修改数据状态的方法传递过去,从而达到子组件更新父组件数据状态并触发视图更新

示例:

父组件:

function Father() {
    const [message, setMessage] = useState("Hello");
    // 调用子组件并传递数据
    return <Child msg={message} setMsg={setMessage} />
}
1
2
3
4
5

子组件:

function Child(props) {
    // 接收来自父组件的 msg 状态数据
    return (
    	<>
        	<div> {props.msg} </div>
        	<button onClick={()=> props.setMsg("React")}>button</button>
        </>
    )
}
1
2
3
4
5
6
7
8
9

或:

function Child({ msg, setMsg }) {
    // 接收来自父组件的 msg 状态数据
    return (
    	<>
        	<div> {msg} </div>
        	<button onClick={()=> setMsg("React")}>button</button>
        </>
    )
}
1
2
3
4
5
6
7
8
9

image-20220325161716935

  • 父组件将初始状态传递给子组件并通过子组件的props或解构的方式渲染用户界面
  • 当用户进行交互操作时,触发父组件的修改状态
  • 修改状态出发后更新数据状态并通过props传递到子组件
  • 最后,视图通信渲染并同步当前状态

5.2 兄弟组件

如果要实现兄弟组件直接的数据状态传递需要借助一种思想:状态提升。

所谓的状态提升就是将兄弟组件直接共享状态提升到最近的公共父组件中,通过公共父组件来维护该状态,然后通过子组件props接收。

示例:

公共父组件:

function Father() {
    const [person, setPerson] = React.useState({ name: "张三" });
    return (
    	<>
        	<ChildFirst person={person} />
        	<ChildSecond person={person} />
        </>
    )
}
1
2
3
4
5
6
7
8
9

兄弟组件:

function ChildFirst({person}) {
    return <div> {person.name} </div>
}
1
2
3
function ChildSecond({person}) {
    return <div> {person.name} </div>
}
1
2
3

5.3 JSX 扩展运算符

在 JSX 中可以使用扩展运算符将对象展开,对象展开后将对象中的每个属性直接传入组件。

示例1:

父组件:

function App() {
  const values = { sayHello: "Hello", sayHi: "hi" };
  return <Message sayHello={values.sayHello} sayHi={values.sayHi} />;
}
1
2
3
4

或:

function App() {
  const values = { sayHello: "Hello", sayHi: "hi" };
  return <Message {...values} />;
}
1
2
3
4

示例2:

父组件:

function App() {
  const values = { sayHello: "Hello", sayHi: "hi" };
  return <Message {...values} />;
}
1
2
3
4

子组件:

function Message(props) {
  return <Button {...props} />;
}
1
2
3

孙子组件:

function Button(props) {
  return <pre>{JSON.stringify(props, null, 2)}</pre>;
}
1
2
3

5.4 Context

目标:

  1. prop drilling 概述
  2. context 概述
  3. context 使用
1. props drilling 概述

在之前的组件状态提升并不适用,并且会使层级关系变得更加复杂,并且使无关的组件参与到了状态传递的过程,我们通常称这种情况为 props drilling

image-20220325161756074

示例:

// Application: 应用页面级组件
function Application() {
  const userName = "John Smith";
  return <Layout userName={userName}>Main content</Layout>;
}
// Layout: 应用布局组件
function Layout({ children, userName }) {
  return (
    <div>
      <Header userName={userName} />
      <main>{children}</main>
    </div>
  );
}
// Header 头部组件
function Header({ userName }) {
  return (
    <header>
      <UserInfo userName={userName} />
    </header>
  );
}
// UserInfo: 用户信息组件
function UserInfo({ userName }) {
  return <span>{userName}</span>;
}
// 渲染应用级页面组件 Application
ReactDOM.render(<Application />, document.getElementById("root"));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2. Context 概述

为了解决以上问题,React 提供了 Context(上下文),允许组件访问全局状态并通过全局状态变化而更新视图。

无论组件嵌套有多深,都可以通过Context 传递数据

image-20220325161810871

3. Context 使用

使用实例:

const value = useContext(MyContext);
1

接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值。即使祖先使用 React.memoshouldComponentUpdate,也会在组件本身使用 useContext 时重新渲染。

别忘记 useContext 的参数必须是 context 对象本身

  • 正确: useContext(MyContext)
  • 错误: useContext(MyContext.Consumer)
  • 错误: useContext(MyContext.Provider)

第一步:通过 createContext 创建 Context,并提供默认值:

// 创建 Context 对象
// createContext 方法的参数就是默认的状态值
const Context = React.createContext("Default Value");
1
2
3

第二步:组件通过 React.useContext 方法获取默认状态

function App() {
  // 组件通过 useContext 方法获取 Context 提供的默认状态
  const value = React.useContext(Context);
  return <div>{value}</div>;
}
1
2
3
4
5

第三步:通过 Context.Provider 组件提供全局状态,实现组件访问。

// 创建 Context 对象存储默认状态值
const Context = React.createContext("Default Value");

function App() {
  const value = "My Context Value";
  // 通过 Context.Provider 组件提供状态, 该状态值会覆盖默认状态值
  return (
    <Context.Provider value={value}>
      <MyComponent />
    </Context.Provider>
  );
}

function MyComponent() {
  // 下层组件获取 Context 全局状态值
  const value = React.useContext(Context);
  return <div>{value}</div>;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

6. 表单控制

目标:

  1. 受控表单组件
  2. 非受控表单组件

6.1 受控表单组件

目标:

  1. 受控表单三步骤

  2. text

  3. password

  4. textarea

  5. select

  6. radio

  7. checkbox

  8. submit

  9. reset

在 React 组件中使用表单元素时,为了获取表单控件的值,常常会将表单控件和组件状态进行绑定,通过该方式使用表单的组件叫做受控表单组件。

0. 受控表单三步骤:
  1. 定义保存 input 值的状态

    const [value, setValue] = useState("");
    
    1
  2. 创建事务处理程序,该事件处理程序在值更改时更新状态

    const handleChange = (event) => setValue(event.target.value);
    
    1
  3. 为 input 分配字段状态值并添加事件处理程序

    <input type="text" value={value} onChange={handleChange} />
    
    1
1. Text 文本框

示例代码:

function App() {
  const [username, setUsername] = React.useState("");
  const handleChange = (event) ={
      setUsername(event.target.value)
  }
  return (
    <input
      type="text"
      value={username}
      onChange={handleChange}
    />
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
2. Password 密码框

原始写法实例:

function App() {
    const [formState, setFormState] = useState({
        usename: "",
        password: "",
    })
    const handleChangeUsername = (event) => {
        setFormState({
            ...formState,
            username: event.target.value
        })
    }
    const handleChangePasswrod = (event) => {
        setFormState({
            ...formState,
            password: event.target.value
        })
    }
    
    return (
    	<>
        	<input 
                type="text" 
                value={formState.username}
                onChange={handleChangeUsername}
               />
        	<input 
                type="password" 
                value={formState.password}
                onChange={handleChangePasswrod}
               />
        </>
    )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

以上这种方法过于麻烦,且代码冗余,因为 event 事件对象我们可以获取到元素的 DOM 对象,可以通过表单组件上的name属性进行进一步的简化过程:

简化代码:

function App() {
    const [formState, setFormState] = useState({
        usename: "",
        password: "",
    })
    const handleChangeFormState = (event) => {
        setFormState({
            ...formState,
            [event.target.name]: event.target.value
        })
    }
    
    return (
    	<>
        	<input 
                type="text" 
                name="username"
                value={formState.username}
                onChange={handleChangeFormState}
               />
        	<input 
                type="password" 
                name="password"
                value={formState.password}
                onChange={handleChangeFormState}
               />
        </>
    )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
3. Textarea 文本域

示例代码:

function App() {
  const [formState, setFormState] = React.useState({
    textarea: "",
  });
  const handleChangeFormState = (event) => {
    setFormState({
      ...formState,
      [event.target.name]: event.target.value,
    });
  };
  return <textarea name="textarea" value={formState.textarea} onChange={onChangeHandler}></textarea>;
}
1
2
3
4
5
6
7
8
9
10
11
12
4. Select 下拉框

示例代码:

function App() {
  const [formState, setFormState] = React.useState({
    select: "",
  });
  const handleChangeFormState = (event) => {
    setFormState({
      ...formState,
      [event.target.name]: event.target.value,
    });
  };
  return (
    <select
      name="select"
      value={formState.select}
      onChange={handleChangeFormState}
      >
      {/* 默认值 */}
      <option value="">请选择交通方式</option>
      <option value="0">火车</option>
      <option value="1">飞机</option>
    </select>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
5. Radio 单选框

示例代码:

function App() {
  const [formState, setFormState] = React.useState({
    radio: "",
  });
  const handleChangeFormState = (event) => {
    setFormState({
      ...formState,
      [event.target.name]: event.target.value,
    });
  };
  return (
    <>
    	<input type="radio" name="radio" value="m"  onChange={handleChangeFormState} />
    	<span>M</span>
    	<input type="radio" name="radio" value="s" onChange={handleChangeFormState} />
    	<span>S</span>
		</>
	);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
6. Checkbox 多选框

目标:

  1. 单独使用
  2. 多个使用
  • checkbox 单独使用,比如是否同意协议、是否记住密码、是否保持登录状态等等,在这种情况下 checkbox 绑定布尔值。

    function App() {
      const [formState, setFormState] = React.useState({
        checkbox: false,
      });
      const handleChangeFormState = (event) => {
        // 如果表单控件是复选框, 获取 checked 属性的值作为 value
        const value =
              event.target.type === "checkbox"
        ? event.target.checked
        : event.target.value;
          
        setFormState({
          ...formState,
          [event.target.name]: value,
        });
      };
      return (
        <input
          type="checkbox"
          name="checkbox"
          checked={formState.checkbox}
          onChange={handleChangeFormState}
          />
      );
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
  • 多个复选框一起使用,表示用户选择了多个属性符合

    // 模拟数据 爱好数组
    const data = [
      { id: 1, title: "足球" },
      { id: 2, title: "篮球" },
      { id: 3, title: "橄榄球" },
    ];
    function App() {
      // 声明表单状态
      const [formState, setFormState] = React.useState({
        hobbies: [],
      });
      // 爱好数组映射, 爱好数组中有多少爱好, 该状态数组中就有多少布尔值与之对应, 表示对应的爱好当前的选中状态是什么
      const [checkedState, setCheckedState] = React.useState(
        new Array(data.length).fill(false)
      );
      // 用于选择爱好后执行的事件处理函数
      const hobbyChangeHandler = (index) => {
        // index 为用户更改的爱好在原数组中的索引
        // 根据 index 找到爱好对应的布尔值, 取反, 返回新的爱好数组对应的是否选中的状态布尔值数组
        const updatedCheckedState = checkedState.map((checked, i) =>  i === index ? !checked : checked);
        // 更新爱好状态布尔值数组, 供下次用户选择时使用
        setCheckedState(updatedCheckedState);
        // 根据爱好布尔值状态数组, 从原数组中找到用户选中的爱好 id
        const udpatedHobbies = updatedCheckedState.reduce(
          (result, checked, index) => {
            if (checked) result.push(data[index].id);
            return result;
          },
          []
        );
        // 更新表单状态
        setFormState({ ...formState, hobbies: udpatedHobbies });
      };
      return (
        <>
          {data.map((item, index) => (
            <p key={item.id}>
              <input
                type="checkbox"
                onChange={() => hobbyChangeHandler(index)}
                />
              {item.title}
            </p>
        	))}
      	</>
      );
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
7. Submit 表单提交

示例代码:

function App() {
  const [formState, setFormState] = React.useState({
    username: "",
  });
  const handleChangeFormState = (event) => {
    setFormState({
      ...formState,
      [event.target.name]: event.target.value,
    });
  };
  const handleSubmit = (event) => {
    event.preventDefault();
    console.log(formState);
  };
  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        name="username"
        value={formState.username}
        onChange={handleChangeFormState}
        />
      <input type="submit" />
    </form>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
8. Reset 重置表单

示例代码:

const initialState = {
  username: "",
};
function App() {
  const [formState, setFormState] = React.useState(initialState);
  const handleChangeFormState = (event) => {
    setFormState({
      ...formState,
      [event.target.name]: event.target.value,
    });
  };
  const onResetHandler = (event) => {
    setFormState(initialState);
  };
  return (
    <form>
      <input
        type="text"
        name="username"
        value={formState.username}
        onChange={handleChangeFormState}
        />
      <button type="button" onClick={onResetHandler}>
        重置
      </button>
    </form>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

2.6.2 非受控表单组件

  1. useRef
  2. forwardRef
  3. 非受控表单组件
1. useRef

使用 useRef 可以获取元素的 DOM 对象

import { useRef } from 'react';

function App () {
  const username = useRef();
  const handler = () => console.log(username); // {current: input}
  return <input ref={username} onChange={handler}/>
}
1
2
3
4
5
6
7
2. forwardRef

通过 forwardRef 可以实现获取子元素的 DOM 对象

父元素:

import { useEffect, useRef } from "react";
import Message from "./Message";

function App() {
  const messageRef = useRef();
  useEffect(() => {
    console.log(messageRef.current);
  }, []);
  return <Message ref={messageRef} />;
}

export default App;
1
2
3
4
5
6
7
8
9
10
11
12

子元素:

import { forwardRef } from "react";

function Message(props, ref) {
  return <span ref={ref}>I am span</span>;
}
export default forwardRef(Message);
1
2
3
4
5
6
3. 非受控表单组件

在 React 中受控表单组件使用起来很方便但也相对复杂,如果表单本身比较简单也可以使用非受控表单组件,非受控表单组件就是表单控件不和组件状态进行绑定,就使用原生的表单 DOM 对象存储用户输入的值。

示例代码:

function App() {
  const usernameRef = React.useRef();
  const onSubmitHandler = (event) => {
    event.preventDefault();
    console.log(usernameRef.current.value);
  };
  return (
    <form onSubmit={onSubmitHandler}>
      <input type="text" ref={usernameRef} />
      <input type="submit" />
    </form>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13

7. 样式控制

目标:

  1. CSS stylesheets
  2. Inline styling
  3. CSS Modules
  4. classnames

React 提供了很多种方式让开发者通过 JS 操作进行CSS的操作。

7.1 CSS stylesheets

在 JS 文件中通过 import 关键字导入 CSS 样式表,改样式表的作用范围是 全局

src/styles.css:

.button {
  color: #fff;
  background-color: #5cb85c;
  text-align: center;
  cursor: pointer;
  padding: 6px 12px;
  font-size: 14px;
  line-height: 1.42857143;
  border-radius: 4px;
  border: none;
}
1
2
3
4
5
6
7
8
9
10
11

src/index:

import "./styles.css";
1

src/App.jsx

function App() {
  return <button class="button">button</button>;
}
1
2
3

7.2 Inline styling

通过 style 属性为元素添加行内式样式,同时可以进行JS逻辑的编写

import { useState } from "react";

function App() {
  const [state, setState] = useState({
    colors: ["palevioletred", "yellow", "papayawhip"],
    index: 0,
  });
  const styles = {
    width: 200,
    padding: "50px 0",
    background: state.colors[state.index],
    textAlign: "center",
  };
  const onClickHandler = () => {
    setState({
      ...state,
      index: state.index + 1 > state.colors.length - 1 ? 0 : state.index + 1,
    });
  };
  return (
    <div style={styles} onClick={onClickHandler}>
      Hello React
    </div>
  );
}

export default App;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

7.3 CSS Module

通过 CSS 模块可以实现组件级样式,样式文件名称规范:[name].module.css

.button {
  color: #fff;
  background-color: #5cb85c;
  text-align: center;
  cursor: pointer;
  padding: 6px 12px;
  font-size: 14px;
  line-height: 1.42857143;
  border-radius: 4px;
  border: none;
}
1
2
3
4
5
6
7
8
9
10
11

src/App.jsx:

// App.js
import styles from "./App.module.css";

function App() {
  return <button className={styles.button}>button</button>;
}
1
2
3
4
5
6

7.4 classnames

通过第三方的插件 classnames 实现属性值的动态绑定

npm install classnames
1

8. 传送门组件

目标:

  1. 已知问题
  2. 组件应用

React Portal 之所以叫 Portal,因为其功能就是和 “传送门” 原理一样:render 到一个组件里面,实际上改变的是网页上另一处的 DOM 结构

其作用就是,当某个组件需要一个弹出框且弹出框不受任何组件的影响,其中的原因就是:如果放置到组件内部会收到组件的 CSS 样式影响如:position、transition等属性。

8.1 已知问题

需求: 在 App 组件中点击按钮渲染弹框组件。

问题: 弹框组件被渲染到 App 组件内部,弹框组件的样式受到了 App 组件元素的影响,导致布局错乱。

期望结果:

28

实际结果:

29

src/App.js:

import { useState } from "react";
import Modal from "./Modal";

function App() {
  const [isOpen, setIsOpen] = useState(false);
  const appStyles = { width: "60%", height: 400, transform: "translate(0,0)" };
  return (
    <div style={appStyles}>
      <button onClick={() => setIsOpen(!isOpen)}>open modal</button>
      {isOpen ? <Modal /> : null}
    </div>
  );
}

export default App;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

src/Modal.js:

import styles from "./Modal.module.css";

function Modal() {
  return (
    <div className={styles.overlayer}>
      <div className={styles.content}></div>
    </div>
  );
}
export default Modal;

1
2
3
4
5
6
7
8
9
10
11

src/Modal.module.css:

.overlayer {
  width: 100%;
  height: 100%;
  position: fixed;
  left: 0;
  top: 0;
  background: rgba(0, 0, 0, 0.5);
}
.content {
  width: 450px;
  height: 300px;
  background: #fff;
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

8.2 组件应用

通过 React 提供的方法,createPortal 将弹出框渲染到指定DOM元素位置。

public/index.html:

<div id="portal-root"></div>
1

src/Modal.jsx:

import styles from "./Modal.module.css";
import ReactDOM from "react-dom";

function Modal() {
  return ReactDOM.createPortal(
    <div className={styles.overlayer}>
      <div className={styles.content}></div>
    </div>,
    document.getElementById("portal-root")
  );
}
export default Modal;
1
2
3
4
5
6
7
8
9
10
11
12